<?php
declare (strict_types=1);
namespace Upload\Driver;
use Config;
use Upload\UploadInterface;
use Yaf\Exception;

class LocalFile implements UploadInterface
{

    /**
     * @var array
     */
    private array $config = [];

    /**
     * 上传错误信息
     * @var string
     */
    private string $error = '';

    /**
     * 构造方法，用于构造上传实例
     * @throws Exception
     */
    public function __construct(array $config = []) {
        try {
            $this->config = Config::get("upload");
            $this->config = array_merge ( $this->config, $config );
        } catch (\Exception $e) {
            throw new Exception($e->getMessage());
        }
    }

    /**
     * 获取最后一次上传错误信息
     *
     * @return string 错误信息
     */
    public function getError(): string
    {
        return $this->error;
    }

    /**
     * 上传单个文件
     *
     * @param array $file 文件数组
     * @return bool|array 上传成功后的文件信息
     */
    public function uploadOne(array $file): bool|array
    {
        $info = $this->upload ( array ($file) );
        return $info ? $info [0] : $info;
    }

    /**
     * 上传文件
     *
     * @param array|string $files 通常是 $_FILES数组
     * @return bool|array
     */
    public function upload(array|string $files = []): bool|array
    {
        if (!$files) $files = $_FILES;
        if (empty ( $files )) {
            $this->error = '没有上传的文件！';
            return false;
        }
        /** 检测上传根目录 */
        if (! $this->checkRootPath ( $this->config['rootPath'] )) {
            $this->error = $this->getError ();
            return false;
        }
        /** 检查上传目录 */
        if (! $this->checkSavePath ( $this->config['savePath'] )) {
            $this->error = $this->getError ();
            return false;
        }
        /** 逐个检测并上传文件 */
        $info = array ();
        if (function_exists ( 'finfo_open' )) {
            $finfo = finfo_open ( FILEINFO_MIME_TYPE );
        }
        // 对上传文件数组信息处理
        $files = $this->dealFiles ( $files );
        foreach ( $files as $key => $file ) {
            $file ['name'] = strip_tags ( $file ['name'] );
            if (! isset ( $file ['key'] )) $file ['key'] = $key;
            /** 通过扩展获取文件类型，可解决FLASH上传$FILES数组返回文件类型错误的问题 */
            if (isset ( $finfo )) {
                $file ['type'] = finfo_file ( $finfo, $file ['tmp_name'] );
            }
            /** 获取上传文件后缀，允许上传无后缀文件 */
            $file ['ext'] = pathinfo ( $file ['name'], PATHINFO_EXTENSION );
            /** 文件上传检测 */
            if (! $this->check ( $file )) continue;
            /** 获取文件hash */
            if ($this->config['hash']) {
                $file ['md5'] = md5_file ( $file ['tmp_name'] );
            }
            /** 生成保存文件名 */
            $savename = $this->getSaveName ( $file );
            if (!$savename) {
                continue;
            } else {
                $file ['savename'] = $savename;
            }
            /* 检测并创建子目录 */
            $subpath = $this->getSubPath ( $file ['name'] );
            if (false === $subpath) {
                continue;
            } else {
                $file ['savepath'] =$this->config['savePath'] . $subpath;
            }
            /** 对图像文件进行严格检测 */
            $ext = strtolower ( $file ['ext'] );
            if (in_array ( $ext, array ('gif', 'jpg', 'jpeg', 'bmp', 'png', 'swf') )) {
                $imgInfo = getimagesize ( $file ['tmp_name'] );
                $file ['width'] = $imgInfo [0];
                $file ['height'] = $imgInfo [1];
                if (empty ( $imgInfo ) || ($ext == 'gif' && empty ( $imgInfo ['bits'] ))) {
                    $this->error = '非法图像文件！';
                    continue;
                }
            }
            /** 保存文件 并记录保存成功的文件 */
            if ($this->save ( $file, !($this->config['replace'] == 'false'))) {
                unset ( $file ['error'], $file ['tmp_name'] );
                $info [$key] = $file;
            } else {
                $this->error = $this->getError ();
            }
        }
        if (isset ( $finfo )) {
            finfo_close ( $finfo );
        }
        return empty ( $info ) ? false : $info;
    }

    /**
     * 转换上传文件数组变量为正确的方式
     *
     * @access private
     * @param array $files
     *        	上传的文件变量
     * @return array
     */
    private function dealFiles(array $files): array
    {
        $fileArray = array ();
        $n = 0;
        foreach ( $files as $key => $file ) {
            if (is_array ( $file ['name'] )) {
                $keys = array_keys ( $file );
                $count = count ( $file ['name'] );
                for($i = 0; $i < $count; $i ++) {
                    $fileArray [$n] ['key'] = $key;
                    foreach ( $keys as $_key ) {
                        $fileArray [$n] [$_key] = $file [$_key] [$i];
                    }
                    $n ++;
                }
            } else {
                $fileArray = $files;
                break;
            }
        }
        return $fileArray;
    }

    /**
     * 检查上传的文件
     *
     * @param array $file
     *        	文件信息
     */
    private function check(array $file): bool
    {
        /** 文件上传失败，捕获错误代码 */
        if ($file ['error']) {
            $this->error ( $file ['error'] );
            return false;
        }

        /** 无效上传 */
        if (empty ( $file ['name'] )) {
            $this->error = '未知上传错误！';
        }

        /** 检查是否合法上传 */
        if (! is_uploaded_file ( $file ['tmp_name'] )) {
            $this->error = '非法上传文件！';
            return false;
        }

        /** 检查文件大小 */
        if (! $this->checkSize ( $file ['size'] )) {
            $this->error = '上传文件大小不符！';
            return false;
        }

        /** 检查文件Mime类型 */
        // TODO:FLASH上传的文件获取到的mime类型都为application/octet-stream
        if (! $this->checkMime ( $file ['type'] )) {
            $this->error = '上传文件MIME类型不允许！';
            return false;
        }

        /** 检查文件后缀 */
        if (! $this->checkExt ( $file ['ext'] )) {
            $this->error = '上传文件后缀不允许';
            return false;
        }

        /** 通过检测 */
        return true;
    }

    /**
     * 获取错误代码信息
     *
     * @param int $errorNo 错误号
     *
     */
    private function error(int $errorNo): void
    {
        $this->error = match ($errorNo) {
            1 => '上传的文件超过了 php.ini 中 upload_max_filesize 选项限制的值！',
            2 => '上传文件的大小超过了 HTML 表单中 MAX_FILE_SIZE 选项指定的值！',
            3 => '文件只有部分被上传！',
            4 => '没有文件被上传！',
            6 => '找不到临时文件夹！',
            7 => '文件写入失败！',
            default => '未知上传错误！',
        };
    }

    /**
     * 检查文件大小是否合法
     * @param int $size
     * @return bool
     */
    private function checkSize(int $size): bool
    {
        return ! ($size > $this->config['maxSize']) || (0 == $this->config['maxSize']);
    }

    /**
     * 检查上传的文件MIME类型是否合法
     * @param string $mime
     * @return bool
     */
    private function checkMime(string $mime): bool
    {
        return empty($this->config['mimes']) || in_array(strtolower($mime), explode(",", $this->config['mimes']));
    }

    /**
     * 检查上传的文件后缀是否合法
     * @param string $ext
     * @return bool
     */
    private function checkExt(string $ext): bool
    {
        return in_array(strtolower($ext), explode(",",$this->config['exts']));
    }

    /**
     * 根据上传文件命名规则取得保存文件名
     * @param array $file
     * @return bool|string
     */
    private function getSaveName(array $file): bool|string
    {
        $rule = $this->config['saveName'];
        if (empty ( $rule )) { // 保持文件名不变
            /** 解决pathinfo中文文件名BUG */
            $filename = substr ( pathinfo ( "_{$file['name']}", PATHINFO_FILENAME ), 1 );
            $saveName = $filename;
        } else {
            $saveName = $this->getName ( $rule, $file ['name'] );
            if (empty ( $saveName )) {
                $this->error = '文件命名规则错误！';
                return false;
            }
        }
        /** 文件保存后缀，支持强制更改文件后缀 */
        $ext =  $this->config['saveExt'] ?: $file ['ext'];
        return $saveName . '.' . $ext;
    }

    /**
     * 获取子目录的名称
     * @param $filename
     * @return bool|string
     */
    private function getSubPath($filename): bool|string
    {
        $subPath = '';
        $rule = $this->config['subName']=='date'?date("Y-m-d"):"";
        if ($this->config['autoSub']=='true' && ! empty ( $rule )) {
            $subPath = $this->getName ( $rule, $filename ) . '/';
            if (! empty ( $subPath ) && ! $this->mkdir ($this->config['savePath'] . $subPath )) {
                $this->error = $this->getError ();
                return false;
            }
        }
        return $subPath;
    }

    /**
     * 根据指定的规则获取文件或目录名称
     *
     * @param mixed $rule 规则
     * @param string $filename 原文件名
     * @return string 文件或目录名称
     */
    private function getName(mixed $rule, string $filename): string
    {
        $name = '';
        $rule  = explode(",", $rule);
        if (count($rule)==1) {
            $rule = $rule[0];
        }
        if (is_array ( $rule )) { // 数组规则
            $func = $rule [0];
            $param = ( array ) $rule [1];
            foreach ( $param as &$value ) {
                $value = str_replace ( '__FILE__', $filename, $value );
            }
            $name = call_user_func_array ( $func, $param );
        } elseif (is_string ( $rule )) { // 字符串规则
            if (function_exists ( $rule )) {
                $name = call_user_func ( $rule );
            } else {
                $name = $rule;
            }
        }
        return $name;
    }

    /**
     * 检测上传根目录
     *
     * @param string $rootPath
     * @return boolean true-检测通过，false-检测失败
     */
    public function checkRootPath(string $rootPath): bool
    {
        if (! (is_dir ( $rootPath ) && is_writable ( $rootPath ))) {
            $this->error = '上传根目录不存在！请尝试手动创建:' . $rootPath;
            return false;
        }
        $this->config['rootPath'] = $rootPath;
        return true;
    }

    /**
     * 检测上传目录
     *
     * @param string $savePath 上传目录
     * @return boolean 检测结果，true-通过，false-失败
     */
    public function checkSavePath(string $savePath): bool
    {
        /** 检测并创建目录 */
        if (! $this->mkdir ( $savePath )) {
            return false;
        } else {
            /** 检测目录是否可写 */
            if (! is_writable ( $this->config['rootPath'] . $savePath )) {
                $this->error = '上传目录 ' . $savePath . ' 不可写！';
                return false;
            } else {
                return true;
            }
        }
    }

    /**
     * 保存指定文件
     *
     * @param array $file     保存的文件信息
     * @param boolean $replace     同名文件是否覆盖
     * @return boolean 保存状态，true-成功，false-失败
     */
    public function save(array $file, bool $replace = true): bool
    {
        $filename = $this->config['rootPath'] . $file ['savepath'] . $file ['savename'];
        /** 不覆盖同名文件 */
        if (! $replace && is_file ( $filename )) {
            $this->error = '存在同名文件' . $file ['savename'];
            return false;
        }
        /** 移动文件 */
        if (! move_uploaded_file ( $file ['tmp_name'], $filename )) {
            $this->error = '文件上传保存错误！';
            return false;
        }
        return true;
    }

    /**
     * 创建目录
     *
     * @param string $savePath
     * @param int $permissions
     * @return boolean 创建状态，true-成功，false-失败
     */
    public function mkdir(string $savePath,int $permissions = 0664): bool
    {
        $dir = $this->config['rootPath'] . $savePath;
        if (is_dir ( $dir )) return true;
        if (mkdir ( $dir, $permissions, true )) {
            return true;
        } else {
            $this->error = "目录 {$savePath} 创建失败！";
            return false;
        }
    }
}
